using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Point = Microsoft.Xna.Framework.Point;
using Color = Microsoft.Xna.Framework.Graphics.Color;
using Keys = Microsoft.Xna.Framework.Input.Keys;
using ButtonState = Microsoft.Xna.Framework.Input.ButtonState;

namespace GP2D3D_Homework1_RickBeijer_S1012261
{
    /// <summary>
    /// This is the main type for the game. It handles keyboard and mouse events, and steers the display mode swapping.
    /// </summary>
    public class Homework1Game : Microsoft.Xna.Framework.Game
    {
        #region Properties
        private GraphicsDeviceManager graphicsDeviceManager;

        private List<CustomDisplayMode> displayModes;
        private readonly CustomDisplayMode defaultDisplayMode;
        private int currentDisplayModeIndex;
        private bool isFullScreen;
        private Size dimensions;

        private bool pauseKeyHandling;
        private TimeSpan elapsedKeyboardTime;
        private bool isShiftDown;
        private bool isCtrlDown;
        private bool isAltDown;

        private bool pauseMouseHandling;
        private bool isMouseDown;
        private Point mouseDownPosition;
        private Point mouseMovePosition;

        /// <summary>
        /// To remember the last pyramid drawn, we keep track of its location and dimension.
        /// </summary>
        private LocationDimensionPair lastPyramid;
        /// <summary>
        /// To remember the last pyramid drawn, we keep track of its color.
        /// </summary>
        private Color lastPyramidColor;

        /// <summary>
        /// The Graphics object on which all drawing is done.
        /// </summary>
        private Graphics graphics;
        #endregion

        #region Initialization
        public Homework1Game()
        {
            this.graphicsDeviceManager = new GraphicsDeviceManager( this );
            Content.RootDirectory = "Content";

            this.IsMouseVisible = true;
            this.graphicsDeviceManager.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>( graphics_PreparingDeviceSettings );

            // Default Display Mode, the mode currently used. If nothing is selected, this could be used as a default option to still run the game.
            defaultDisplayMode = new CustomDisplayMode( GraphicsAdapter.DefaultAdapter.DeviceName,
                                                        GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Format,
                                                        GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width,
                                                        GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height,
                                                        GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.RefreshRate );
            isFullScreen = false;

            pauseKeyHandling = false;
            elapsedKeyboardTime = new TimeSpan();
            isShiftDown = false;
            isCtrlDown = false;
            isAltDown = false;

            pauseMouseHandling = false;
            isMouseDown = false;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // Get all display modes, even if there are multiple devices.
            displayModes = new List<CustomDisplayMode>();
            int index = 0;

            foreach ( GraphicsAdapter adapter in GraphicsAdapter.Adapters )
            {
                foreach ( DisplayMode displayMode in adapter.SupportedDisplayModes )
                {
                    // Filter out the 32 bits color depth display modes.
                    if (displayMode.Format == SurfaceFormat.Bgr32)
                    {
                        CustomDisplayMode tempDisplayMode = new CustomDisplayMode( adapter.DeviceName,
                                                                                   displayMode.Format,
                                                                                   displayMode.Width,
                                                                                   displayMode.Height,
                                                                                   displayMode.RefreshRate );
                        displayModes.Add( tempDisplayMode );

                        // If this display mode is the default, remember its index.
                        if ( tempDisplayMode.DeviceName == defaultDisplayMode.DeviceName &&
                             tempDisplayMode.GetPixelDepth() == defaultDisplayMode.GetPixelDepth() &&
                             tempDisplayMode.GetWidth() == defaultDisplayMode.GetWidth() &&
                             tempDisplayMode.GetHeight() == defaultDisplayMode.GetHeight() &&
                             tempDisplayMode.RefreshRate == defaultDisplayMode.RefreshRate )
                        {
                            currentDisplayModeIndex = index;
                        }

                        index++;
                    }
                }
            }

            // Make a Graphics object so we can draw on it. The standard viewpoint is 2D, and the pointsize is 5 (otherwise we can't see single pixels).
            graphics = new Graphics(GraphicsDevice, defaultDisplayMode.GetWidth(), defaultDisplayMode.GetHeight(), defaultDisplayMode.GetWidth());
            graphics.CurrentViewPoint = ViewPoint.TwoD;
            graphics.GraphicsDevice.RenderState.PointSize = 5;

            // Change the display mode, according to what the user whishes to use.
            ChangeDisplayMode();

            base.Initialize();
        }
        #endregion

        #region DisplayMode Setting
        /// <summary>
        /// Allows the user to pick a display mode from a list of available display modes. The list is based on the hardware of the
        /// current computer.
        /// </summary>
        private void ChangeDisplayMode()
        {
            // Pause input handling due to the dialog box. This dialog will handle its own events.
            pauseKeyHandling = true;
            pauseMouseHandling = true;

            // Request a display mode from the user.
            DisplayModeSelectionDialog displayModeSelectionDialogDialog = new DisplayModeSelectionDialog(displayModes, currentDisplayModeIndex);
            DialogResult dialogResult = displayModeSelectionDialogDialog.ShowDialog();

            // If nothing is selected, close the game and return from this method before applying settings (which may me null).
            if (dialogResult != DialogResult.OK)
            {
                this.Exit();
                return;
            }
            
            // If there was a mode selected, apply it.
            currentDisplayModeIndex = displayModeSelectionDialogDialog.SelectedDisplayModeIndex;
            this.ChangeDisplayMode(displayModeSelectionDialogDialog.SelectedDisplayMode);

            // Enable the input handling again.
            pauseKeyHandling = false;
            pauseMouseHandling = false;
        }

        /// <summary>
        /// Change the display mode according to the passed parameters.
        /// </summary>
        /// <param name="newDisplayMode">The display mode to set.</param>
        private void ChangeDisplayMode(CustomDisplayMode newDisplayMode)
        {
            // Since a new width and height has been set, we need to remember this.
            dimensions = new Size( newDisplayMode.GetWidth(), newDisplayMode.GetHeight() );

            // Because of the new width and height, we need to let the Graphics know what's happening. We also need to reset any viewpoints
            // we previously set, because they are based on the width, height and depth of the backbuffer.
            graphics.Width = dimensions.Width;
            graphics.Height = dimensions.Height;
            graphics.Depth = dimensions.Width;
            graphics.ResetProjection();

            // Change the settings of the display mode.
            graphicsDeviceManager.IsFullScreen = isFullScreen;
            graphicsDeviceManager.PreferredBackBufferFormat = newDisplayMode.GetPixelDepth();
            graphicsDeviceManager.PreferredBackBufferWidth = newDisplayMode.GetWidth();
            graphicsDeviceManager.PreferredBackBufferHeight = newDisplayMode.GetHeight();

            // Apply the new display mode.
            graphicsDeviceManager.ApplyChanges();

            // Calling the invalidate here makes sure the screen doesn't flikker after turning to full screen.
            graphics.Invalidate();
        }

        /// <summary>
        /// Sets some basic options for the display parameters. This event is only fired at startup and switch from fullscreen to windowed and back.
        /// </summary>
        /// <param name="sender">The object which sent this event.</param>
        /// <param name="e">The preparation parameters of the device being prepared.</param>
        protected void graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            e.GraphicsDeviceInformation.PresentationParameters.EnableAutoDepthStencil = true;
            e.GraphicsDeviceInformation.PresentationParameters.MultiSampleQuality = 0;
            e.GraphicsDeviceInformation.PresentationParameters.MultiSampleType = MultiSampleType.FourSamples;
        }
        #endregion

        #region Content Load/Unload
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Not used.
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // Not used.
        }
        #endregion

        #region Updating
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            HandleKeyEvents( gameTime );
            HandleMouseEvents( gameTime );

            // If the viewpoint of the graphics is 3D, we need to keep rotating it with every update.
            if (graphics.CurrentViewPoint == ViewPoint.ThreeD)
            {
                graphics.ViewRotation++;
                graphics.SetBasic3DViewPoint();
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Handles all keyboard keys being pressed. The GameTime here is used to ensure that a user doesn't keep a key pressed for
        /// an unintended amount of updates. If we don't use the GameTime to set some kind of interval between key presses, the user
        /// would not be able to operate this game correctly. Also, if the key handling should be paused (pauseKeyHandling), then
        /// nothing is done in this method.
        /// </summary>
        /// <param name="gameTime">The GameTime from which to determine the elapsed game time between updates.</param>
        protected void HandleKeyEvents(GameTime gameTime)
        {
            // If the key handling is already paused when entering this update, do nothing.
            if (pauseKeyHandling)
            {
                return;
            }

            // Make sure the keyboard events are only handled once (keyboard keys tend to stay pushed for a few milliseconds...).
            elapsedKeyboardTime += gameTime.ElapsedGameTime;

            // If the allowed response time has not been reached, do nothing.
            if ( elapsedKeyboardTime.Milliseconds < 100 )
            {
                return;
            }

            // If the allowed response time has been reached, reset the timer and handle the key events.
            elapsedKeyboardTime = new TimeSpan();

            // Get the current keyboard state.
            KeyboardState keyboardState = Keyboard.GetState();

            // Register the special keys for use in the mousedrawing.
            isShiftDown = keyboardState.IsKeyDown( Keys.LeftShift ) || keyboardState.IsKeyDown( Keys.RightShift );
            isCtrlDown = keyboardState.IsKeyDown( Keys.LeftControl ) || keyboardState.IsKeyDown( Keys.RightControl );
            isAltDown = keyboardState.IsKeyDown( Keys.LeftAlt ) || keyboardState.IsKeyDown( Keys.RightAlt );

            // If the F1 key is pressed, change the point draw style.
            if (keyboardState.IsKeyDown(Keys.F1))
            {
                graphics.PointDrawStyle++;

                if (graphics.PointDrawStyle > PrimitiveType.TriangleStrip)
                {
                    graphics.PointDrawStyle = PrimitiveType.PointList;
                }
            }
            // If the F1 key is pressed, change the cube draw style.
            if (keyboardState.IsKeyDown(Keys.F2))
            {
                graphics.CubeDrawStyle++;

                if (graphics.CubeDrawStyle > PrimitiveType.TriangleStrip)
                {
                    graphics.CubeDrawStyle = PrimitiveType.LineList;
                }
            }
            // If the F1 key is pressed, change the pyramid draw style.
            if (keyboardState.IsKeyDown(Keys.F3))
            {
                graphics.PyramidDrawStyle++;

                if (graphics.PyramidDrawStyle > PrimitiveType.TriangleStrip)
                {
                    graphics.PyramidDrawStyle = PrimitiveType.LineList;
                }
            }
            // If the F4 key is pressed, change the Fillmode of the drawing.
            if (keyboardState.IsKeyDown(Keys.F4))
            {
                graphics.GraphicsDevice.RenderState.FillMode++;

                if (graphics.GraphicsDevice.RenderState.FillMode > FillMode.Solid)
                {
                    graphics.GraphicsDevice.RenderState.FillMode = FillMode.Point;
                }
            }
            // On F10, allow the user to pick a new display mode.
            if ( keyboardState.IsKeyDown( Keys.F10 ) )
            {
                ChangeDisplayMode();
            }
            // On F11, toggle between full screen and windowed mode.
            if ( keyboardState.IsKeyDown( Keys.F11 ) )
            {
                isFullScreen = !isFullScreen;

                ChangeDisplayMode( displayModes[currentDisplayModeIndex] );
            }
            // Check for F12 (in which the user cannot draw, but can only view the game).
            if (keyboardState.IsKeyDown(Keys.F12))
            {
                if (graphics.CurrentViewPoint == ViewPoint.TwoD)
                {
                    graphics.CurrentViewPoint = ViewPoint.ThreeD;
                }
                else if (graphics.CurrentViewPoint == ViewPoint.ThreeD)
                {
                    graphics.CurrentViewPoint = ViewPoint.TwoD;
                }
            }
            // On left, change the display mode to a lower one.
            if ( keyboardState.IsKeyDown( Keys.Left ) )
            {
                currentDisplayModeIndex--;

                if ( currentDisplayModeIndex < 0 )
                {
                    currentDisplayModeIndex = displayModes.Count - 1;
                }

                ChangeDisplayMode( displayModes[currentDisplayModeIndex] );
            }
            // On right, change the display mode to a higher one.
            if ( keyboardState.IsKeyDown( Keys.Right ) )
            {
                currentDisplayModeIndex++;

                if ( currentDisplayModeIndex > displayModes.Count - 1 )
                {
                    currentDisplayModeIndex = 0;
                }

                ChangeDisplayMode( displayModes[currentDisplayModeIndex] );
            }
            // If the up key is pressed, increase the height of the last pyramid.
            if (keyboardState.IsKeyDown(Keys.Up))
            {
                if (lastPyramid != null)
                {
                    graphics.RemovePyramid( lastPyramid.Location, lastPyramid.Dimension );

                    lastPyramid = new LocationDimensionPair( new Vector3( lastPyramid.Location.X,
                                                                          lastPyramid.Location.Y - 5,
                                                                          lastPyramid.Location.Z ),
                                                             new Vector3( lastPyramid.Dimension.X,
                                                                          lastPyramid.Dimension.Y + 5,
                                                                          lastPyramid.Dimension.Z ) );

                    graphics.AddPyramid( lastPyramid.Location, lastPyramid.Dimension, lastPyramidColor );
                }
            }
            // If the down key is pressed, decrease the height of the last pyramid.
            if (keyboardState.IsKeyDown(Keys.Down))
            {
                if (lastPyramid != null)
                {
                    graphics.RemovePyramid(lastPyramid.Location, lastPyramid.Dimension);

                    lastPyramid = new LocationDimensionPair(new Vector3(lastPyramid.Location.X,
                                                                          lastPyramid.Location.Y + 5,
                                                                          lastPyramid.Location.Z),
                                                             new Vector3(lastPyramid.Dimension.X,
                                                                          lastPyramid.Dimension.Y - 5,
                                                                          lastPyramid.Dimension.Z));

                    graphics.AddPyramid(lastPyramid.Location, lastPyramid.Dimension, lastPyramidColor);
                }
            }
            // If the delete key is pressed, clear the screen of all shapes.
            if ( keyboardState.IsKeyDown( Keys.Delete ) )
            {
                graphics.Clear();
            }
            // On escape, exit the game.
            if ( keyboardState.IsKeyDown( Keys.Escape ) )
            {
                Exit();
                return;
            }
        }
        
        /// <summary>
        /// Handles all mouse events, from mouse key presses to mouse position. The GameTime is passed here as a variable that may be
        /// used to create a delay in the handling of certain events. It is not used here.
        /// </summary>
        /// <param name="gameTime">The GameTime from which to determine the elapsed game time between updates.</param>
        protected void HandleMouseEvents(GameTime gameTime)
        {
            // If the mouse event handling was paused, do nothing.
            if ( pauseMouseHandling )
            {
                return;
            }

            // Get the mouse state.
            MouseState mouseState = Mouse.GetState();
            // Make a point, we'll almost always need it. We need to subtract half the width and height from the mouse location, because
            // the graphics' center point is 0, 0 (it ranges from -width to width, and height to -height).
            Point mouseClick = new Point( mouseState.X - (dimensions.Width / 2), mouseState.Y - (dimensions.Height / 2) );

            // If in the last update the mouse was not down, but now it is, record the position where this happened.
            if ( !isMouseDown && mouseState.LeftButton == ButtonState.Pressed )
            {
                mouseDownPosition = mouseClick;
            }
            // If the mouse is down (Mouse Move event), record the position of the mouse.
            if ( isMouseDown )
            {
                mouseMovePosition = mouseClick;
            }

            // If in the last update the mouse was down, but it was just released, do something.
            if ( ( isMouseDown && mouseState.LeftButton == ButtonState.Released && !isShiftDown && !isAltDown ) ||
                 ( isMouseDown && isCtrlDown && !isShiftDown && !isAltDown ) )
            {
                graphics.AddPixel( new Vector3( mouseClick.X, mouseClick.Y, 0 ), Color.Green );
            }
            // If in the last update the mouse was down, but it was just released, draw a permanent cube. If it was not yet released,
            // draw a temporary cube.
            else if ( ( isMouseDown && mouseState.LeftButton == ButtonState.Released && isShiftDown ) ||
                      ( isMouseDown && isShiftDown ) )
            {
                Vector3 location = new Vector3( mouseDownPosition.X, mouseDownPosition.Y, 2 );
                Vector3 previousDimension = new Vector3(mouseMovePosition.X - location.X, mouseMovePosition.Y - location.Y, mouseMovePosition.X - location.X);
                Vector3 newDimension = new Vector3(mouseClick.X - location.X, mouseClick.Y - location.Y, mouseClick.X - location.X);

                // Remove the previous cube if the user is still drawing it.
                if ( isShiftDown )
                {
                    graphics.RemoveCube( location, previousDimension );
                }

                graphics.AddCube( location, newDimension, Color.Firebrick );
            }
            // If the user is trying to draw with alt pressed, draw a pyramid.
            else if ( ( isMouseDown && mouseState.LeftButton == ButtonState.Released && isAltDown ) ||
                      ( isMouseDown && isAltDown ) )
            {
                Vector3 location = new Vector3( mouseDownPosition.X, mouseDownPosition.Y, 1 );
                Vector3 previousDimension = new Vector3(mouseMovePosition.X - location.X, mouseMovePosition.Y - location.Y, mouseMovePosition.X - location.X);
                Vector3 newDimension = new Vector3(mouseClick.X - location.X, mouseClick.Y - location.Y, mouseClick.X - location.X);

                // Remove the previous pyramid if the user is still drawing it.
                if ( isAltDown )
                {
                    graphics.RemovePyramid( location, previousDimension );
                }

                // Remember the last pyramid drawn, to be able to change the height with keyboard controls.
                lastPyramid = new LocationDimensionPair( location, newDimension );
                lastPyramidColor = Color.Goldenrod;

                graphics.AddPyramid( location, newDimension, lastPyramidColor );
            }

            // Set whether the mouse is down or not.
            isMouseDown = mouseState.LeftButton == ButtonState.Pressed;
        }
        #endregion

        #region Drawing
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            // To make the single pixels visible:
            graphics.GraphicsDevice.RenderState.PointSize = 5;
            // To make the pyramids and cubes completely visible, even when drawing from right to left:
            graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
            // Draw everything:
            graphics.Invalidate();
        }
        #endregion
    }
}